﻿// -----------------------------------------------------------------------
// <copyright file="UsingExpression.cs" company="none">
// Copyright (c) 2012 M. Alberti, xanatos(at)live.it
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
// </copyright>
// -----------------------------------------------------------------------

namespace FluentStatement
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq.Expressions;
    using System.Reflection;
    using Utilities;

    /// <summary>
    /// 
    /// </summary>
    public class UsingExpression : Expression
    {
        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo IDisposableDispose = typeof(IDisposable).GetMethod("Dispose");

        /// <summary>
        /// 
        /// </summary>
        protected readonly Expression Disposable2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly Expression Body2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly Expression Expression2;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="disposable"></param>
        /// <param name="body"></param>
        public UsingExpression(Expression disposable, Expression body)
        {
            if (disposable == null)
            {
                throw new ArgumentNullException("disposable");
            }

            if (!typeof(IDisposable).IsAssignableFrom(disposable.Type))
            {
                throw new ArgumentException("@object");
            }

            if (body == null)
            {
                throw new ArgumentNullException("body");
            }

            var body2 = body as LambdaExpression;

            if (body2 != null && (body2.Parameters.Count != 1 || !typeof(IDisposable).IsAssignableFrom(body2.Parameters[0].Type)))
            {
                throw new ArgumentException("body");
            }

            this.Disposable2 = disposable;
            this.Body2 = body;

            this.Expression2 = this.InternalReduce();
        }

        /// <summary>
        /// 
        /// </summary>
        public Expression Disposable
        {
            get
            {
                return this.Disposable2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public Expression Body
        {
            get
            {
                return this.Body2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override ExpressionType NodeType
        {
            get
            {
                return ExpressionType.Extension;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override bool CanReduce
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override Type Type
        {
            get
            {
                var body2 = this.Body as LambdaExpression;

                if (body2 != null)
                {
                    return body2.Body.Type;
                }

                return this.Body.Type;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override Expression Reduce()
        {
            return this.Expression2;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        protected Expression InternalReduce()
        {
            var blockVariables = new List<ParameterExpression>();
            var block = new List<Expression>();

            ParameterExpression @object = Expression.Variable(this.Disposable.Type);

            blockVariables.Add(@object);

            bool nonNullableValueType = this.Disposable.Type.IsNullable();

            // Nullable value types and reference types.
            if (!nonNullableValueType)
            {
                block.Add(Expression.Assign(@object, this.Disposable));
            }

            var body2 = this.Body as LambdaExpression;

            if (body2 != null)
            {
                blockVariables.Add(body2.Parameters[0]);
                block.Add(Expression.Assign(body2.Parameters[0], @object));
                block.Add(body2.Body);
            }
            else
            {
                block.Add(this.Body);
            }

            Expression expression;

            if (nonNullableValueType)
            {
                // With non-nullable value types the Expression.Assign is done BEFORE the try/catch,
                // and no null check is done at the end.
                expression = Expression.Block(
                    blockVariables,
                    new Expression[] 
                    { 
                        Expression.Assign(@object, this.Disposable),
                        Expression.TryFinally(
                            Expression.Block(block), 
                            Expression.Call(@object, IDisposableDispose)) 
                    });
            }
            else if (this.Disposable.Type.IsValueType)
            {
                // Nullable value type
                PropertyInfo hasValue = this.Disposable.Type.GetProperty("HasValue");
                PropertyInfo value = this.Disposable.Type.GetProperty("Value");

                expression = Expression.Block(
                    blockVariables,
                    new Expression[] 
                    { 
                        Expression.TryFinally(
                            Expression.Block(block), 
                            Expression.IfThen(
                                Expression.Property(@object, hasValue), 
                                Expression.Call(Expression.Property(@object, value), IDisposableDispose))) 
                    });
            }
            else
            {
                // reference type
                expression = Expression.Block(
                    blockVariables,
                    new Expression[] 
                    { 
                        Expression.TryFinally(
                            Expression.Block(block), 
                            Expression.IfThen(
                                Expression.ReferenceNotEqual(@object, Expression.Constant(null)), 
                                Expression.Call(@object, IDisposableDispose))) 
                    });
            }

            return expression;
        }
    }
}